/* This module abstracts the SOS page table itself, providing walk functions */

#include "include.h"

/* Data relating to a single (faulting) page walk */
typedef struct page_walk_data {
    sos_pcb *pcb;
    sos_page_table *pt;
    seL4_Word pt_idx;
    page_attrs attrs;
    page_table_cb cb;
    void *cb_data;
    int err;
} page_walk_data;


static int _pte_is_alloced(page_table_entry pte);
static int _attrs_are_empty(page_attrs attrs);
static int _sos_page_alloc_finalise(void *data);
static void _page_table_frame_alloc_cb(seL4_Word vaddr, void *data);
int page_walk_nofault(sos_pcb *pcb, seL4_Word proc_vaddr,
                             page_table_entry **pte);
int page_walk(sos_pcb *pcb, seL4_Word proc_vaddr, page_attrs attrs,
                     page_table_cb cb, void *cb_data);


int sos_page_lookup_nofault(sos_pcb *pcb, seL4_Word proc_vaddr,
                            seL4_Word *sos_vaddr, page_attrs *attrs) {
    if (!pcb || !sos_vaddr || !(pcb->sos_pd)) {
        return SOS_PAGE_TABLE_INVAL;
    } 

    // page_walk_nofault
    int err;
    page_table_entry *pte;
    err = page_walk_nofault(pcb, proc_vaddr, &pte);
    if (err) return err;

    // Check the frame is mapped
    if (pte->attrs.swapped || pte->framenumber == 0) {
        // Would require fault to proceed
        return SOS_PAGE_TABLE_FAULT;
    }

    // Address translation, copy attrs and return
    (*sos_vaddr) = frame_translate_number(pte->framenumber);
    if (attrs) (*attrs) = pte->attrs;
    return SOS_PAGE_TABLE_SUCCESS;
}


/* Get a pointer to the PTE for proc_vaddr, without faulting.                */
/* i.e. this succeeds if the page table corresponding to the address exists. */
int page_walk_nofault(sos_pcb *pcb, seL4_Word proc_vaddr,
                             page_table_entry **pte) {
    int err = SOS_PAGE_TABLE_SUCCESS;
    seL4_Word pd_idx;
    seL4_Word pt_idx;
    sos_page_table *pt;

    if (!pcb || !pte || !(pcb->sos_pd))
        return SOS_PAGE_TABLE_INVAL;

    // Grab PD and PT indices from the given vaddr
    pd_idx = PAGE_TABLE(proc_vaddr);
    pt_idx = PAGE_TABLE_ENTRY(proc_vaddr);

    // Index into PD and PT, which must both exist
    pt = (sos_page_table *) ((pcb->sos_pd) + pd_idx);

    dprintf(7, "page_walk_nofault: page directory exists at %p\n", pcb->sos_pd);

    // No page table = fault required
    if(!(*pt)) return SOS_PAGE_TABLE_FAULT;

    dprintf(7, "page_walk_nofault: page table exists\n");

    (*pte) = (page_table_entry *) ((*pt) + pt_idx);

    // Couple of debug prints
    dprintf(6, "I think my PTE is at %p\n", *pte);
    dprintf(8, "sizeof(struct page_table_entry) = %d\n",
                sizeof(struct page_table_entry));
    dprintf(6, "FN: %d\n", (*pte)->framenumber);
    dprintf(8, "page_walk_nofault: returning\n");

    return err;
}

/* A faulting page walk. May need to alloc various frames for SOS and seL4
   page tables. */
int page_walk(sos_pcb *pcb, seL4_Word proc_vaddr, page_attrs attrs,
              page_table_cb cb, void *cb_data) {
    int err;
    seL4_Word pd_idx;

    dprintf(6, "page_walk: seeking %p\n", proc_vaddr);

    // malloc a structure to store all intermediate data
    page_walk_data *data = malloc(sizeof(struct page_walk_data));
    if (data == NULL) {
        dprintf(1, "sos_page_alloc: Could not allocate callback data (OOM)\n");
        return SOS_PAGE_TABLE_OOM;
    }

    // Stash the attrs and callbacks for later
    data->attrs = attrs;
    data->cb = cb;
    data->cb_data = cb_data;
    data->pcb = pcb;

    // Sanity checks - if these fail, process has been corrupted
    // These are ok - no need to check for abort since synchronous call
    conditional_panic(((void*) pcb->sos_pd == NULL), "No page directory");
    conditional_panic((pcb->cspace == NULL), "Process cspace does not exist");

    // Grab PD and PT indices from the given vaddr
    pd_idx = PAGE_TABLE(proc_vaddr);
    data->pt_idx = PAGE_TABLE_ENTRY(proc_vaddr);

    // Index into page directory, which *must* exist (else process is corrupt)
    // pt stores the address of the pointer to the PT, i.e. (**)

    data->pt = (sos_page_table *) (pcb->sos_pd + pd_idx);

    dprintf(6, "page_walk: setting up alloc or finaliser\n");

    if ((*(data->pt)) == NULL) {
        // PT we want doesn't exist, so we alloc it
        dprintf(6, "page_walk: time for frame_alloc\n");
        err = frame_alloc(&_page_table_frame_alloc_cb, (void*) data);
        // frame_alloc is asynchronous - it will finalise for us
        return err;
    } else {
        // Return to syscall loop, then finalise
        dprintf(6, "page_walk: ready to finalise\n");
        err = sos_task_add_ready(&_sos_page_alloc_finalise, (void *) data,
                                 SOS_TASK_PRIORITY_HIGH);
        if (err) {
            dprintf(1, "sos_page_alloc: Could not finalise (%d)\n", err);
            // XXX Could tear down the process here
            free(data);
        }
        return SOS_PAGE_TABLE_SUCCESS;
    }
}

/* The second half of page_walk, run once we're sure we have a SOS PT */
static int _sos_page_alloc_finalise(void *_data) {
    page_walk_data *data = (page_walk_data *) _data;
    page_table_entry *pte;

    // We've lost a race - let's not crash
    // XXX Somebody with more time on their hands would clean up the frame
    if (data->pcb->abort) {
        dprintf(6, "page_alloc_finalise: aborting\n");
        (*(data->cb))(SOS_PAGE_TABLE_ABORT, data->cb_data);
        free(data);
        return SOS_TASK_SUCCESS;
    }

    // Index into page table, which must exist.
    pte = (page_table_entry *) ((*(data->pt)) + data->pt_idx);
    dprintf(5, "I think my PTE is at %p\n", pte);
    dprintf(8, "sizeof(struct page_table_entry) = %d\n", sizeof(struct page_table_entry));

    if (pte->framenumber == 0) {
        // Update the PTE with relevant things
        dprintf(5, "Setting PTE and attrs...\n");
        pte->attrs = data->attrs;
    }

    dprintf(6, "page_walk: calling back\n");

    // Call back whoever is waiting
    (*(data->cb))(SOS_PAGE_TABLE_SUCCESS, data->cb_data);
    free(data);
    return SOS_TASK_SUCCESS;
}

/* The callback passed to frame_alloc. XXX Why two functions instead of one? */
static void _page_table_frame_alloc_cb(seL4_Word vaddr, void *_data) {
    page_walk_data *data = (page_walk_data *) _data;

    if (data->pcb->abort) {
        dprintf(6, "page_table_frame_alloc_cb: aborting\n");
        (*(data->cb))(SOS_PAGE_TABLE_ABORT, data->cb_data);
        free(data);
        return;
    }

    if (vaddr == 0) {
        // We're completely out of memory.
        (*(data->cb))(SOS_PAGE_TABLE_OOM, data->cb_data);
        free(data);
        return;
    } else {
        // Finalise the operation
        dprintf(5, "Allocated frame for page table at %p\n", (void *) vaddr);
        (*(data->pt)) = (void *) vaddr;
        _sos_page_alloc_finalise(data);
    }
}

static inline int _attrs_are_empty(page_attrs attrs) {
    return (attrs.swapped == 0 && attrs.writeable == 0 &&
            attrs.readable == 0 && attrs.executable == 0 &&
            attrs.pinned == 0);
}

static inline int _pte_is_alloced(page_table_entry pte) {
    return *((seL4_Word *) &pte) != 0;
}
